Micron Document
C.S.Burner 🪙 ━►🔥GIT Node

Node / reticulum_mirror / MeshChatX / files / meshchatx / src / frontend / components / tools / PaperMessagePage.vue

Displaying Raw • Download

meshchatx/src/frontend/components/tools/PaperMessagePage.vue 9ec2a88817819d433c5d68dbec0ca070ca989b91 (9ec2a888) Text, 30.45 KB

T8b949e<!-- SPDX-License-Identifier: 0BSD -->

Tff7b72<templateTff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-col flex-1 overflow-hidden min-w-0 bg-slate-50 dark:bg-zinc-950Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 overflow-y-auto w-full pb-[max(1rem,env(safe-area-inset-bottom))]Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffp-3 sm:p-4 md:p-6 max-w-5xl mx-auto w-full space-y-4 min-w-0Ta5d6ff"Tff7b72>
T8b949e<!-- header -->
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffborder-b border-gray-200 dark:border-zinc-800 pb-4Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex items-start gap-3 min-w-0Ta5d6ff"Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffp-2 bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 rounded-lg shrink-0Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffqrcodeTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-6Ta5d6ff" Tff7b72/>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmin-w-0Ta5d6ff"Tff7b72>
Tff7b72<h2 Te6edf3class=Ta5d6ff"Ta5d6fftext-lg sm:text-xl font-bold text-gray-900 dark:text-white tracking-tightTa5d6ff"Tff7b72>
Paper Message Generator
Tff7b72</h2>
Tff7b72<p Te6edf3class=Ta5d6ff"Ta5d6fftext-xs sm:text-sm text-gray-600 dark:text-gray-400 mt-1Ta5d6ff"Tff7b72>
Generate signed LXMF messages for physical delivery or offline transfer.
Tff7b72</p>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>

Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffgrid grid-cols-1 lg:grid-cols-2 gap-4Ta5d6ff"Tff7b72>
T8b949e<!-- composer -->
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffspace-y-4 min-w-0Ta5d6ff"Tff7b72>
Tff7b72<section
Te6edf3class=Ta5d6ff"Ta5d6ffrounded-lg border border-gray-200 dark:border-zinc-800 overflow-hidden bg-white dark:bg-zinc-950Ta5d6ff"
Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffpx-4 py-3 border-b border-gray-200 dark:border-zinc-800 bg-gray-50/80 dark:bg-zinc-900/50Ta5d6ff"
Tff7b72>
Tff7b72<h2
Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-2 text-base font-semibold text-gray-900 dark:text-whiteTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon
Te6edf3icon-name=Ta5d6ff"Ta5d6ffpencil-outlineTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffsize-5 text-gray-400 shrink-0Ta5d6ff"
Tff7b72/>
Compose Message
Tff7b72</h2>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffpx-4 py-4 space-y-3 text-gray-900 dark:text-gray-100Ta5d6ff"Tff7b72>
Tff7b72<divTff7b72>
Tff7b72<label
Te6edf3class=Ta5d6ff"Ta5d6ffblock text-[10px] font-bold text-gray-400 dark:text-zinc-500 uppercase tracking-widest mb-1.5Ta5d6ff"
Tff7b72>
Recipient Address
Tff7b72</label>
Tff7b72<input
Te6edf3v-model=Ta5d6ff"Ta5d6ffdestinationHashTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6fftextTa5d6ff"
Te6edf3placeholder=Ta5d6ff"Ta5d6ffDestination hash (e.g. a39610...)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinput-field font-mono text-smTa5d6ff"
Te6edf3maxlength=Ta5d6ff"Ta5d6ff32Ta5d6ff"
Tff7b72/>
Tff7b72</div>
Tff7b72<divTff7b72>
Tff7b72<label
Te6edf3class=Ta5d6ff"Ta5d6ffblock text-[10px] font-bold text-gray-400 dark:text-zinc-500 uppercase tracking-widest mb-1.5Ta5d6ff"
Tff7b72>
Subject (Optional)
Tff7b72</label>
Tff7b72<input
Te6edf3v-model=Ta5d6ff"Ta5d6fftitleTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6fftextTa5d6ff"
Te6edf3placeholder=Ta5d6ff"Ta5d6ffMessage title...Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinput-field text-smTa5d6ff"
Tff7b72/>
Tff7b72</div>
Tff7b72<divTff7b72>
Tff7b72<label
Te6edf3class=Ta5d6ff"Ta5d6ffblock text-[10px] font-bold text-gray-400 dark:text-zinc-500 uppercase tracking-widest mb-1.5Ta5d6ff"
Tff7b72>
Message Content
Tff7b72</label>
Tff7b72<textarea
Te6edf3v-model=Ta5d6ff"Ta5d6ffcontentTa5d6ff"
Te6edf3rows=Ta5d6ff"Ta5d6ff4Ta5d6ff"
Te6edf3placeholder=Ta5d6ff"Ta5d6ffType your message here...Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinput-field resize-none text-smTa5d6ff"
Tff7b72>Tff7b72</textarea>
Tff7b72</div>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-full flex items-center justify-center gap-2 py-2.5 px-4 bg-blue-600 hover:bg-blue-700 text-white rounded-xl font-bold shadow-lg shadow-blue-500/20 transition-all active:scale-[0.98] disabled:opacity-50 disabled:pointer-events-none text-smTa5d6ff"
Te6edf3:disabled=Ta5d6ff"Ta5d6ff!canGenerate || isGeneratingTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffgeneratePaperMessageTa5d6ff"
Tff7b72>
Tff7b72<template Te6edf3v-if=Ta5d6ff"Ta5d6ffisGeneratingTa5d6ff"Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffsize-4 border-2 border-white/20 border-t-white rounded-full animate-spinTa5d6ff"
Tff7b72>Tff7b72</div>
Generating...
Tff7b72</template>
Tff7b72<template v-elseTff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffqrcode-plusTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-5Ta5d6ff" Tff7b72/>
Generate Paper Message
Tff7b72</template>
Tff7b72</button>
Tff7b72</div>
Tff7b72</section>

T8b949e<!-- read / ingest section -->
Tff7b72<section
Te6edf3class=Ta5d6ff"Ta5d6ffrounded-lg border border-gray-200 dark:border-zinc-800 overflow-hidden bg-white dark:bg-zinc-950Ta5d6ff"
Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffpx-4 py-3 border-b border-gray-200 dark:border-zinc-800 bg-gray-50/80 dark:bg-zinc-900/50Ta5d6ff"
Tff7b72>
Tff7b72<h2
Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-2 text-base font-semibold text-gray-900 dark:text-whiteTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffqrcode-scanTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-5 text-gray-400 shrink-0Ta5d6ff" Tff7b72/>
Ingest Paper Message
Tff7b72</h2>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffpx-4 py-4 space-y-3 text-gray-900 dark:text-gray-100Ta5d6ff"Tff7b72>
Tff7b72<p Te6edf3class=Ta5d6ff"Ta5d6fftext-xs text-gray-600 dark:text-gray-400Ta5d6ff"Tff7b72>
Paste an LXMF, LXMA, or LXM URI to decode and ingest.
Tff7b72</p>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-col sm:flex-row gap-2Ta5d6ff"Tff7b72>
Tff7b72<input
Te6edf3v-model=Ta5d6ff"Ta5d6ffingestUriTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6fftextTa5d6ff"
Te6edf3placeholder=Ta5d6ff"Ta5d6fflxmf://... or lxma://...Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinput-field flex-1 min-w-0 font-mono text-smTa5d6ff"
@keydown.Te6edf3enter=Ta5d6ff"Ta5d6ffingestPaperMessageTa5d6ff"
Tff7b72/>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinline-flex items-center justify-center gap-2 px-3 py-2.5 sm:py-2 bg-gray-100 dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 rounded-lg hover:bg-gray-200 dark:hover:bg-zinc-700 transition-colors shrink-0Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffpasteFromClipboardTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffcontent-pasteTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-5Ta5d6ff" Tff7b72/>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6ffsm:hidden text-sm font-mediumTa5d6ff"Tff7b72>PasteTff7b72</span>
Tff7b72</button>
Tff7b72<button
Te6edf3v-if=Ta5d6ff"Ta5d6ffcameraSupportedTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinline-flex items-center justify-center gap-2 px-3 py-2.5 sm:py-2 bg-gray-100 dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 rounded-lg hover:bg-gray-200 dark:hover:bg-zinc-700 transition-colors shrink-0Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffopenIngestScannerModalTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffqrcode-scanTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-5Ta5d6ff" Tff7b72/>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6ffsm:hidden text-sm font-mediumTa5d6ff"Tff7b72>{{ Te6edf3$t("messages.scan_qr") }}Tff7b72</span>
Tff7b72</button>
Tff7b72</div>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-full py-2.5 px-4 bg-gray-100 dark:bg-zinc-800 text-gray-700 dark:text-zinc-200 rounded-xl font-bold hover:bg-gray-200 dark:hover:bg-zinc-700 transition-all active:scale-[0.98] text-smTa5d6ff"
Te6edf3:disabled=Ta5d6ff"Ta5d6ff!ingestUriTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffingestPaperMessageTa5d6ff"
Tff7b72>
Read LXM
Tff7b72</button>
Tff7b72<p Te6edf3v-if=Ta5d6ff"Ta5d6ff!cameraSupportedTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6fftext-xs text-gray-500 dark:text-zinc-400Ta5d6ff"Tff7b72>
{{ Te6edf3$t("messages.camera_not_supported") }}
Tff7b72</p>
Tff7b72</div>
Tff7b72</section>
Tff7b72</div>

T8b949e<!-- preview / result -->
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffspace-y-4 min-w-0Ta5d6ff"Tff7b72>
Tff7b72<section
Te6edf3v-if=Ta5d6ff"Ta5d6ffgeneratedUriTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffrounded-lg border border-gray-200 dark:border-zinc-800 overflow-hidden bg-white dark:bg-zinc-950Ta5d6ff"
Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffpx-4 py-3 border-b border-gray-200 dark:border-zinc-800 bg-blue-50/80 dark:bg-blue-900/20Ta5d6ff"
Tff7b72>
Tff7b72<h2 Te6edf3class=Ta5d6ff"Ta5d6fftext-base font-semibold text-blue-600 dark:text-blue-400Ta5d6ff"Tff7b72>
Generated QR Code
Tff7b72</h2>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffpx-4 py-4 sm:p-6 flex flex-col items-center text-gray-900 dark:text-gray-100Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffp-3 bg-white rounded-2xl shadow-inner border border-gray-100 mb-6Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffsize-40 sm:size-48 flex items-center justify-center overflow-hiddenTa5d6ff"Tff7b72>
Tff7b72<canvas Te6edf3ref=Ta5d6ff"Ta5d6ffqrcodeTa5d6ff"Tff7b72>Tff7b72</canvas>
Tff7b72</div>
Tff7b72</div>

Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffw-full space-y-3Ta5d6ff"Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffbg-gray-50 dark:bg-zinc-800/50 rounded-2xl p-3 border border-gray-100 dark:border-zinc-700/50Ta5d6ff"
Tff7b72>
Tff7b72<label
Te6edf3class=Ta5d6ff"Ta5d6ffblock text-[9px] font-bold text-gray-400 dark:text-zinc-500 uppercase tracking-widest mb-1.5Ta5d6ff"
Tff7b72>
LXMF URI
Tff7b72</label>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex gap-2Ta5d6ff"Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 font-mono text-[10px] break-all text-gray-600 dark:text-zinc-300 bg-white dark:bg-zinc-900 p-2 rounded-lg border border-gray-200 dark:border-zinc-700 max-h-20 overflow-y-autoTa5d6ff"
Tff7b72>
{{ generatedUri }}
Tff7b72</div>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffsize-9 flex items-center justify-center bg-white dark:bg-zinc-900 text-gray-500 dark:text-zinc-400 rounded-lg border border-gray-200 dark:border-zinc-700 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 transition-all shadow-smTa5d6ff"
Te6edf3title=Ta5d6ff"Ta5d6ffCopy URITa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffcopyUriTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffcontent-copyTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>

Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-col sm:flex-row gap-2 pt-1Ta5d6ff"Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 flex items-center justify-center gap-2 py-3 sm:py-2.5 px-4 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-bold transition-all active:scale-[0.98] text-sm min-h-[44px]Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffprintQRCodeTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffprinterTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4Ta5d6ff" Tff7b72/>
Print
Tff7b72</button>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 flex items-center justify-center gap-2 py-3 sm:py-2.5 px-4 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg font-bold transition-all active:scale-[0.98] text-sm min-h-[44px]Ta5d6ff"
Te6edf3:disabled=Ta5d6ff"Ta5d6ffisSendingTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffsendPaperMessageTa5d6ff"
Tff7b72>
Tff7b72<template Te6edf3v-if=Ta5d6ff"Ta5d6ffisSendingTa5d6ff"Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffsize-4 border-2 border-white/20 border-t-white rounded-full animate-spinTa5d6ff"
Tff7b72>Tff7b72</div>
Sending...
Tff7b72</template>
Tff7b72<template v-elseTff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffsendTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4Ta5d6ff" Tff7b72/>
Send
Tff7b72</template>
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</section>

Tff7b72<div
v-else
Te6edf3class=Ta5d6ff"Ta5d6ffrounded-lg border border-dashed border-gray-300 dark:border-zinc-700 bg-gray-50/50 dark:bg-zinc-900/30 flex flex-col items-center justify-center p-6 sm:p-8 text-center min-h-[240px] sm:min-h-[280px] sm:h-[320px]Ta5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffp-3 bg-gray-100 dark:bg-zinc-800 text-gray-400 rounded-full mb-3Ta5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffqrcodeTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-10Ta5d6ff" Tff7b72/>
Tff7b72</div>
Tff7b72<h3 Te6edf3class=Ta5d6ff"Ta5d6fftext-base font-bold text-gray-900 dark:text-white mb-1Ta5d6ff"Tff7b72>No QR Code GeneratedTff7b72</h3>
Tff7b72<p Te6edf3class=Ta5d6ff"Ta5d6fftext-xs text-gray-500 dark:text-gray-400 max-w-[200px]Ta5d6ff"Tff7b72>
Fill out the message details and click generate to create a signed paper message.
Tff7b72</p>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>

Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffisIngestScannerModalOpenTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6fffixed inset-0 z-[210] flex items-center justify-center p-4 bg-black/70 backdrop-blur-smTa5d6ff"
@click.Te6edf3self=Ta5d6ff"Ta5d6ffcloseIngestScannerModalTa5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffw-full max-w-xl rounded-2xl bg-white dark:bg-zinc-900 shadow-2xl overflow-hiddenTa5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffpx-5 py-4 border-b border-gray-100 dark:border-zinc-800 flex items-center justify-betweenTa5d6ff"Tff7b72>
Tff7b72<h3 Te6edf3class=Ta5d6ff"Ta5d6fftext-lg font-bold text-gray-900 dark:text-zinc-100Ta5d6ff"Tff7b72>{{ Te6edf3$t("messages.scan_qr") }}Tff7b72</h3>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6fftext-gray-400 hover:text-gray-600 dark:hover:text-zinc-300Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffcloseIngestScannerModalTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffcloseTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-5Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffp-5 space-y-3Ta5d6ff"Tff7b72>
Tff7b72<video
Te6edf3ref=Ta5d6ff"Ta5d6ffingestScannerVideoTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-full rounded-xl bg-black max-h-[60vh]Ta5d6ff"
autoplay
playsinline
muted
Tff7b72>Tff7b72</video>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-sm text-gray-500 dark:text-zinc-400Ta5d6ff"Tff7b72>
{{ ingestScannerError || Te6edf3$t("messages.scanner_hint") }}
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</template>

Tff7b72<scriptTff7b72>
import QRCode from "qrcode";
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import WebSocketConnection from "../../js/WebSocketConnection";
import ToastUtils from "../../js/ToastUtils";

export default {
name: "PaperMessagePage",
components: { MaterialDesignIcon },
data() {
return {
destinationHash: "",
title: "",
content: "",
isGenerating: false,
generatedUri: null,
ingestUri: "",
isSending: false,
isIngestScannerModalOpen: false,
ingestScannerError: null,
ingestScannerStream: null,
ingestScannerAnimationFrame: null,
};
},
computed: {
canGenerate() {
return this.destinationHash.length === 32 && this.content.length > 0;
},
cameraSupported() {
return (
typeof window !== "undefined" &&
typeof window.BarcodeDetector !== "undefined" &&
navigator?.mediaDevices?.getUserMedia
);
},
},
mounted() {
WebSocketConnection.on("message", this.onWebsocketMessage);
},
beforeUnmount() {
WebSocketConnection.off("message", this.onWebsocketMessage);
this.stopIngestScanner();
},
methods: {
async onWebsocketMessage(message) {
const json = JSON.parse(message.data);
if (json.type === "lxm.generate_paper_uri.result") {
this.isGenerating = false;
if (json.status === "success") {
this.generatedUri = json.uri;
this.$nextTick(() => {
this.renderQRCode();
});
} else {
ToastUtils.error(json.message);
}
} else if (json.type === "lxm.ingest_uri.result") {
if (json.status === "success") {
this.ingestUri = "";
}
}
},
async generatePaperMessage() {
if (!this.canGenerate) return;

this.isGenerating = true;
this.generatedUri = null;

WebSocketConnection.send(
JSON.stringify({
type: "lxm.generate_paper_uri",
destination_hash: this.destinationHash,
content: this.content,
title: this.title,
})
);
},
async renderQRCode() {
if (!this.generatedUri || !this.$refs.qrcode) return;

try {
await QRCode.toCanvas(this.$refs.qrcode, this.generatedUri, {
width: 256,
margin: 2,
color: {
dark: "#000000",
light: "#ffffff",
},
errorCorrectionLevel: "L",
});

const el = this.$refs.qrcode;
if (el) {
el.style.maxWidth = "100%";
el.style.height = "auto";
el.classList.add("rounded-lg");
}
} catch (err) {
console.error("Failed to render QR code:", err);
ToastUtils.error(this.$t("messages.failed_render_qr"));
}
},
async ingestPaperMessage() {
if (!this.ingestUri) return;

WebSocketConnection.send(
JSON.stringify({
type: "lxm.ingest_uri",
uri: this.ingestUri,
})
);
},
async pasteFromClipboard() {
try {
this.ingestUri = await navigator.clipboard.readText();
} catch {
ToastUtils.error(this.$t("messages.failed_read_clipboard"));
}
},
async openIngestScannerModal() {
this.ingestScannerError = null;
this.isIngestScannerModalOpen = true;
await this.$nextTick();
await this.startIngestScanner();
},
closeIngestScannerModal() {
this.isIngestScannerModalOpen = false;
this.stopIngestScanner();
},
async startIngestScanner() {
if (!this.cameraSupported) {
this.ingestScannerError = this.$t("messages.camera_not_supported");
return;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "environment" },
audio: false,
});
this.ingestScannerStream = stream;
const video = this.$refs.ingestScannerVideo;
if (!video) {
this.ingestScannerError = this.$t("messages.camera_failed");
this.stopIngestScanner();
return;
}
video.srcObject = stream;
await video.play();
this.detectIngestQrLoop();
} catch (e) {
this.ingestScannerError = this.describeCameraError(e);
}
},
detectIngestQrLoop() {
if (!this.isIngestScannerModalOpen) return;
const video = this.$refs.ingestScannerVideo;
if (!video || video.readyState Tff7b72< 2) {
Te6edf3this.ingestScannerAnimationFrame = Ta5d6ffrequestAnimationFrame(() =Tff7b72> this.detectIngestQrLoop());
return;
}
const detector = new window.BarcodeDetector({ formats: ["qr_code"] });
detector
.detect(video)
.then((barcodes) => {
const qr = barcodes?.[0]?.rawValue?.trim();
if (!qr) {
this.ingestScannerAnimationFrame = requestAnimationFrame(() => this.detectIngestQrLoop());
return;
}
if (!/^lxm(a|f)?:\/\//i.test(qr)) {
ToastUtils.error(this.$t("messages.invalid_qr_uri"));
this.ingestScannerAnimationFrame = requestAnimationFrame(() => this.detectIngestQrLoop());
return;
}
this.ingestUri = qr;
this.closeIngestScannerModal();
this.ingestPaperMessage();
})
.catch(() => {
this.ingestScannerAnimationFrame = requestAnimationFrame(() => this.detectIngestQrLoop());
});
},
stopIngestScanner() {
if (this.ingestScannerAnimationFrame) {
cancelAnimationFrame(this.ingestScannerAnimationFrame);
this.ingestScannerAnimationFrame = null;
}
if (this.ingestScannerStream) {
this.ingestScannerStream.getTracks().forEach((track) => track.stop());
this.ingestScannerStream = null;
}
},
describeCameraError(error) {
const name = error?.name || "";
if (name === "NotAllowedError" || name === "SecurityError") {
return this.$t("messages.camera_permission_denied");
}
if (name === "NotFoundError" || name === "DevicesNotFoundError") {
return this.$t("messages.camera_not_found");
}
return this.$t("messages.camera_failed");
},
async copyUri() {
try {
await navigator.clipboard.writeText(this.generatedUri);
ToastUtils.success(this.$t("messages.uri_copied"));
} catch {
ToastUtils.error(this.$t("messages.failed_copy_uri"));
}
},
downloadQRCode() {
const canvas = this.$refs.qrcode;
if (!canvas) return;

const dataUrl = canvas.toDataURL("image/png");
if (dataUrl) {
const link = document.createElement("a");
link.download = `lxmf-paper-message-${Date.now()}.png`;
link.href = dataUrl;
link.click();
}
},
async sendPaperMessage() {
if (!this.destinationHash || !this.generatedUri) return;

try {
this.isSending = true;

// build lxmf message
const lxmf_message = {
destination_hash: this.destinationHash,
content: `Please scan the attached Paper Message or manually ingest this URI: ${this.generatedUri}`,
fields: {},
};

// get data url from canvas if available
const canvas = this.$refs.qrcode;
if (canvas) {
const dataUrl = canvas.toDataURL("image/png");
const imageBytes = dataUrl.split(",")[1];
lxmf_message.fields.image = {
image_type: "png",
image_bytes: imageBytes,
name: "paper_message_qr.png",
};
}

// send message
const response = await window.api.post(`/api/v1/lxmf-messages/send`, {
delivery_method: "opportunistic",
lxmf_message: lxmf_message,
});

if (response.data.lxmf_message) {
ToastUtils.success(this.$t("messages.paper_message_sent"));
this.generatedUri = null;
this.destinationHash = "";
this.content = "";
this.title = "";
} else {
ToastUtils.error(response.data.message || "Failed to send paper message");
}
} catch (err) {
console.error("Failed to send paper message:", err);
ToastUtils.error(this.$t("messages.failed_send_paper"));
} finally {
this.isSending = false;
}
},
printQRCode() {
const canvas = this.$refs.qrcode;
if (!canvas) return;

const dataUrl = canvas.toDataURL("image/png");
if (!dataUrl) return;

const printWindow = window.open("", "_blank");
if (!printWindow) {
ToastUtils.error("Pop-up blocked. Please allow pop-ups to print.");
return;
}

printWindow.document.write(`
Tff7b72<htmlTff7b72>
Tff7b72<headTff7b72>
Tff7b72<titleTff7b72>LXMF Paper MessageTff7b72</title>
Tff7b72<styleTff7b72>
body { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; font-family: sans-serif; }
img { width: 400px; height: 400px; margin-bottom: 20px; }
.hash { font-family: monospace; font-size: 12px; color: #666; }
@media print { body { height: auto; padding: 20px; } }
Tff7b72</style>
Tff7b72</head>
Tff7b72<bodyTff7b72>
Tff7b72<h1Tff7b72>LXMF Paper MessageTff7b72</h1>
Tff7b72<img Te6edf3src=Ta5d6ff"${dataUrl}" Tff7b72/>
Tff7b72<div Te6edf3class=Ta5d6ff"hash"Tff7b72>Recipient: ${this.destinationHash}Tff7b72</div>
Tff7b72<pTff7b72>Scan this code with an LXMF-compatible app to read the message.Tff7b72</p>
Tff7b72<scriptTff7b72>window.onload = () => { window.print(); window.close(); }</scr' + 'ipt>
Tff7b72</body>
Tff7b72</html>
`);
printWindow.document.close();
},
},
};
Tff7b72</script>

Tff7b72<style scopedTff7b72>
.input-field {
@apply bg-gray-50/90 dark:bg-zinc-800/80 border border-gray-200 dark:border-zinc-700 text-sm rounded-2xl focus:ring-2 focus:ring-blue-400 focus:border-blue-400 dark:focus:ring-blue-50 dark:focus:border-blue-500 block w-full p-2.5 text-gray-900 dark:text-gray-100 transition;
}
Tff7b72</style>

Served by rngit 1.5.2 - Generated in 0.05s